package com.shi.aspect;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.shi.annotation.cache.MyCacheable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.Duration;
import java.util.Random;

@Aspect
@Component
@Order(5)
public class CacheableAspect {

    private static final Random RANDOM = new Random();
    /**
     * 默认10秒
     */
    @Value("${cache.lock-ttl:10000}")
    private Integer lock_ttl;
    /**
     * 默认30分钟
     */
    @Value("${cache.object-ttl:1800000}")
    private Integer cache_ttl;
    /**
     * 空值缓存过期时间10秒
     */
    @Value("${cache.null-ttl:10000}")
    private Integer null_ttl;

    @Value("${cache.lock-key-prefix:lock}")
    private String lockKey_prefix;
    /**
     * 全限定类名
     * 确保这个类中有 data属性，因为在重建缓存函数中用到了
     */
    @Value("${cache.result.type:com.shi.common.Result}")
    private String cacheResultType;

    /**
     * 统一返回结果集中，data的属性名
     * 这里默认使用的data，你可以根据需要修改
     */
    @Value("${cache.result.data.name:data}")
    private String dataName;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Pointcut("@annotation(com.shi.annotation.cache.MyCacheable)")
    public void pointCut() {
    }


    @Around("pointCut()")
    public Object handle(ProceedingJoinPoint joinPoint) {
        String key = parseSpEl(joinPoint);
        String cacheStr = stringRedisTemplate.opsForValue().get(key);
        // 检查缓存中是否存在
        if (cacheStr != null) {
            try {
                Class<?> clazz = Class.forName(cacheResultType);
                return JSONUtil.toBean(cacheStr, clazz);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException();
            }
        }
        String lockKey = lockKey_prefix + ":" + key;
        // 重建缓存
        return buildCache(lockKey, key, joinPoint);
    }

    /**
     * 通过互斥锁重建缓存
     *
     * @param lockKey
     * @param key
     * @param joinPoint
     * @return
     */
    private Object buildCache(String lockKey, String key, ProceedingJoinPoint joinPoint) {
        // 获取互斥锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMillis(lock_ttl));
        Object res = null;
        if (Boolean.TRUE.equals(success)) {
            try {
                Class<?> clazz = Class.forName(cacheResultType);
                Field field = clazz.getDeclaredField(dataName);
                field.setAccessible(true);
                res = joinPoint.proceed();
                if (ObjectUtil.isEmpty(field.get(res))) {
                    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(res), Duration.ofMillis(null_ttl));
                    stringRedisTemplate.delete(lockKey);
                    return res;
                }
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(res), Duration.ofMillis(getRandomTTL()));
                stringRedisTemplate.delete(lockKey);
                return res;
            } catch (Throwable e) {
                throw new RuntimeException();
            }
        } else {
            try {
                Thread.sleep(50);
                handle(joinPoint);
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        }
        return null;
    }

    /**
     * 获取缓存的随机过期时间
     *
     * @return
     */
    private Integer getRandomTTL() {
        // 1000*60*10
        // 在原来缓存时间的基础上，随机加上 0-10分钟
        return cache_ttl + RANDOM.nextInt(600000);
    }

    /**
     * 解析注解 @MyCacheable中属性key的SpEl
     *
     * @param joinPoint
     * @return
     */
    private String parseSpEl(ProceedingJoinPoint joinPoint) {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        MyCacheable annotation = method.getAnnotation(MyCacheable.class);
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = method.getParameters();
        // 向SpEL上下文注入参数信息
        for (int i = 0; i < parameters.length; i++) {
            context.setVariable(parameters[i].getName(), args[i]);
        }
        // 使用解析器，在上下文中解析目标表达式
        Object value = parser.parseExpression(annotation.key()).getValue(context);
        if (value == null) {
            return "";
        }
        return value.toString();
    }
}
